/* NOTICE
    OpenJaWS - Open Java Weather Station
    
    Copyright (C) 2008 Grant Gardner <grant@lastweekend.com.au>
    
    OpenJaWS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
NOTICE *//**
 * 
 */

package au.com.lastweekend.ws2300;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.Scanner;

/**
 * Represents the raw memory behind the WS2300 which is addressable by nibble (4 bits)
 * 
 * 
 * @author Grant Gardner <grant@lastweekend.com.au>
 * 
 */
public abstract class Device {

    /**
     * Last writable address is 13FE (on WS2307), 13FF is always zero. Any byte beyond that returns the value for 13FE, 13FF
     */
    public static final int MAX_ADDRESS = 0x13FF;

    /**
     * Dump to a human readable form.
     * 
     * @param writer
     * @param fromAddress
     * @param toAddress
     *            because we can only read in multiples of two, we may read one past.
     * @throws IOException
     */
    public final void dump(Writer writer, int fromAddress, int toAddress) throws IOException {

        if (toAddress < fromAddress) {
            throw new IllegalArgumentException(String.format("fromAddress %04x less than toAddress %04x", fromAddress, toAddress));
        }
        
        int numBytes = ((toAddress - fromAddress)+1)/2;
        byte[] nibbles = read(fromAddress,numBytes);
        writer.write("Address : Value\n");
        for (int i = 0; i < nibbles.length; i++) {
            writer.write(String.format("%04x : %1x\n", i, nibbles[i]));
        }
        writer.flush();
    }
    
    /**
     * Load from a dump. I've never tried this with the real device and I'd suggest you don't either.
     * @param reader
     * @throws IOException
     */
    public void load(Reader reader) throws IOException {

        Scanner scanner = new Scanner(reader);
        scanner.useDelimiter("\\s*:\\s*|\\s*\n");
        scanner.useRadix(16);
        
        byte[] nextNibble = new byte[1];
        while (scanner.hasNextLine()) {
            if("Address".equals(scanner.findInLine("Address"))) {
                scanner.nextLine();
                continue;
            }
            int nextAddress = scanner.nextInt();
            nextNibble[0] = (byte) scanner.nextInt();
            scanner.nextLine();
            write(nextAddress, nextNibble);
        }
        scanner.close();
    }

    /**
     * Read nibbles from the given address.
     * 
     * @param nibbleAddress
     *            0000 - {@link MAX_ADDRESS}
     * @param numBytes
     * 
     * @return the read nibbles ( 2 * numBytes !!)
     * @throws IOException
     */
    public abstract byte[] read(int address, int numBytes) throws IOException;

    /**
     * Write nibbles
     * 
     * @param address
     *            0000 - {@link MAX_ADDRESS}
     * @param nibbles
     *            the nibbles to write (in low nibble of each byte)
     * @throws IOException
     */
    public abstract void write(int address, byte[] nibbles) throws IOException;

    /**
     * 
     * @param nibbleAddress
     *            0000 - {@link MAX_ADDRESS}
     * @param bitAddress
     *            0 - 3
     * @param setBit
     *            true - set, false - unset
     * @throws IOException
     */
    public abstract void writeBit(int nibbleAddress, int bitAddress, boolean setBit) throws IOException;

    
    /**
     * Encode an integer into bcd nibbles.
     * @param value
     * @param nibbles
     * @param offset
     * @param length
     */
    public static void bcdEncode(int value, byte[] nibbles, int offset,int length) {
        for (int i = 0; i < length ; i++) {
            nibbles[i+offset] = (byte) (value % 10);
            value = value / 10;
        }
                    
    }

    /**
     * Convert binary value stored in nibbles to a long
     * @param nibbles
     * @param offset
     * @param length
     * @return
     */
    public static long binary(byte[] nibbles, int offset, int length) {
    
        return nibbleCoded(nibbles, offset, length, 16);
    }

    /**
     * Extract binary coded decimal value from nibbles
     * @param nibbles
     * @param offset
     * @param length
     * @return
     */
    public static int bcd(byte[] nibbles, int offset, int length) {
    
        return nibbleCoded(nibbles, offset, length, 10);
    }

    private static int nibbleCoded(byte[] nibbles, int offset, int length, int radix) {
    
        int result = 0;
        int power = 1;
        for (int i = 0; i < length; i++) {
            result = result + nibbles[offset + i] * power;
            power = power * radix;
        }
        return result;
    }
    
    /**
     * Until 1.6 gives us this as an IOException constructor.
     * @param msg
     * @param t
     * @return
     */
    public static IOException newIOException(String msg, Throwable t) {
        IOException ex = new IOException(msg);
        ex.initCause(t);
        return ex;
    }

}
